/*
 * FlatFileDatabase.java
 * 
 * @version 1.0, April 4, 2011
 * CS 2340 Team 10: Cyberdyne Systems
 */
package edu.gatech.oad.cyberdoc.database;

import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.json.JSONObject;

/**
 * An implementation of the Database interface using flat-files.
 * 
 * @author John Girata
 * @version 1.0, April 4, 2011
 */
public class FlatFileDatabase implements Database {

	/**
	 * The logger for this class.
	 */
	private static final Logger LOG;

	/**
	 * The superpackage of all packages containing classes that are stored in
	 * this database. This string should include a trailing period. If there are
	 * multiple top-level packages, leave this string empty.
	 */
	public static final String TOP_LEVEL_PACKAGE = "edu.gatech.oad.cyberdoc.";

	/**
	 * The top-level directory for this flat-file database.
	 */
	public static final String DIRECTORY = "db/";

	static {
		LOG = Logger
				.getLogger("edu.gatech.oad.cyberdoc.database.FlatFileDatabase");
	}

	/**
	 * Saves an object to the database.
	 * 
	 * @param object
	 *            The object to save
	 * @throws DatabaseException
	 *             If an error occurs while saving this object
	 */
	@Override
	public void save(DatabaseSerializable object) throws DatabaseException {
		final String json = object.toJSONObject().toString();
		String name = object.getClass().getName();
		name = name.substring(TOP_LEVEL_PACKAGE.length());

		final File directory = new File(DIRECTORY + name);

		if (!directory.exists() && !directory.mkdirs()) {
			LOG.severe("Cannot create database directory: " + directory);

			throw new DatabaseException("Cannot create database directory: "
					+ directory);
		}

		try {
			final String uid = object.getUniqueIdentifier();

			if (uid == null || uid.equals("")) {
				throw new DatabaseException(
						"Unique identifier cannot be null (in class "
								+ TOP_LEVEL_PACKAGE + name + ")");
			}

			final File file = new File(directory, uid);
			final FileWriter writer = new FileWriter(file + ".db");

			writer.write(json);
			writer.close();
		} catch (IOException e) {
			LOG.log(Level.SEVERE, "Cannot create database directory: "
					+ directory, e);
		}
	}

	/**
	 * Loads all objects from the database by reading each entry into a
	 * JSONObject and calling each class's constructor.
	 * 
	 * @return The number of objects retrieved from the database
	 * @throws DatabaseException
	 *             If an error occurs while loading the database
	 */
	@Override
	public int load() throws DatabaseException {
		final File database = new File(DIRECTORY);
		int count = 0;

		if (!database.exists() || !database.canRead()) {
			throw new DatabaseException(
					"Database directory does not exist or is not readable.");
		}

		final String[] list = database.list(SVNFilter.getInstance());

		Class<?> jsonClass;

		try {
			jsonClass = Class.forName("org.json.JSONObject");
		} catch (ClassNotFoundException e) {
			LOG.log(Level.SEVERE, "Unexpected exception", e);
			throw new DatabaseException(e);
		}

		for (String classSuffix : list) {
			File classDirectory = new File(database, classSuffix);

			for (String filename : classDirectory.list(SVNFilter.getInstance())) {
				try {
					File file = new File(classDirectory, filename);
					Scanner scanner = new Scanner(file);
					StringBuffer json = new StringBuffer();

					while (scanner.hasNext()) {
						json.append(scanner.nextLine() + '\n');
					}

					json.delete(json.length() - 1, json.length());

					Class<?> definition = Class.forName(TOP_LEVEL_PACKAGE
							+ classSuffix);
					Constructor<?> constructor = definition
							.getConstructor(jsonClass);
					constructor.newInstance(new JSONObject(json.toString()));
					count++;
				} catch (Exception e) {
					LOG.log(Level.SEVERE,
							"Failed to load object from the database", e);
				}
			}
		}

		return count;
	}

	/**
	 * Removes all entries from this database. In this implementation, the
	 * top-level directory is not deleted.
	 */
	@Override
	public void clear() {
		final File directory = new File(DIRECTORY);

		if (directory.exists()) {
			clear(directory);
		}
	}

	/**
	 * Removes the file specified, along with any subfiles. For the purposes of
	 * this method, a file may refer to file or directory on the system.
	 * 
	 * @param file
	 *            The file to remove
	 */
	private static void clear(File file) {
		if (file.isFile()) {
			if (!file.delete()) {
				LOG.severe("Could not delete file " + file
						+ " from the database");
			}
		} else {
			for (String filename : file.list()) {
				final File child = new File(file, filename);
				clear(child);
			}
		}
	}

	/**
	 * Returns a string representation of this object.
	 * 
	 * @return A string representation of this object
	 */
	@Override
	public String toString() {
		return "FlatFileDatabase";
	}

	/**
	 * A filename filter to prevent the database from attempting to initialize
	 * SVN's file.
	 * 
	 * @author John Girata
	 * @version 1.0, April 29, 2011
	 */
	private static class SVNFilter implements FilenameFilter {

		/**
		 * The singleton instance of this class.
		 */
		private static SVNFilter instance;

		/**
		 * Determines if this file is a valid database file.
		 * 
		 * @param dir
		 *            The directory of the file
		 * @param name
		 *            The name of the file in question
		 * @return true if this is a legal database file, or false otherwise
		 */
		@Override
		public boolean accept(File dir, String name) {
			return !name.equals(".svn");
		}

		/**
		 * 
		 * @return
		 */
		public static SVNFilter getInstance() {
			if (instance == null) {
				instance = new SVNFilter();
			}

			return instance;
		}
	}
}
